package com.yvan.js;

import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import jdk.nashorn.internal.runtime.ECMAException;
import jdk.nashorn.internal.runtime.Source;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;

import javax.script.*;
import java.io.File;
import java.util.HashMap;
import java.util.Map;

import static com.yvan.js.JQuery.$;

@Slf4j
public class YvanJS {

    static {
        //注册序列化方法
        ScriptWrapperSerializer.register();
    }

    private static final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();

    @Getter
    private final YvanJsProperties properties;
    @Getter
    private final ModuleCache cache;
    private final DefineFunction define;
    private final RequireFunction require;
    @Getter
    private final ScriptEngine scriptEngine;
    private final Bindings bindings;

    public YvanJS(YvanJsProperties properties) throws ScriptException {
        this.properties = properties;
        // 处理 basePath
        String basePath = this.properties.getBase();
        if (StringUtils.isBlank(StringUtils.trim(basePath))) {
            this.properties.setBase("./");
        } else if (!basePath.endsWith("/")) {
            this.properties.setBase(basePath + "/");
        }
        // 设置缓存实现
        cache = new MemoryModuleCache();
        // 设置 define 实现
        define = new DefaultDefine(this);
        // 设置 require 实现
        require = new DefaultRequire(this);
        // 设置JS引擎 以及 全局Bindings
        NashornScriptEngineFactory nsf = null;
        for (ScriptEngineFactory f : scriptEngineManager.getEngineFactories()) {
            if (f.getEngineName().equalsIgnoreCase("Oracle Nashorn")) {
                nsf = (NashornScriptEngineFactory) f;
                break;
            }
        }
        if (nsf == null) {
            throw new RuntimeException("NashornScriptEngineFactory Not found");
        }
        scriptEngine = nsf.getScriptEngine("-doe");
        scriptEngine.eval("function definitionNewObject() {return {}}");
        bindings = scriptEngine.createBindings();
        bindings.put("$", $);
        bindings.put("define", define);
        scriptEngine.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
    }

    public Object putGlobal(String key, Object value) {
        return bindings.put(key, value);
    }

    public ScriptObjectMirror use(String name) {
        return require.require(name);
    }

    public void clear() {
        cache.clear();
        log.info("# 清空所有缓存 | 当前缓存数量 [{}]", cache.cacheSize());
    }

    private ScriptObjectMirror createSafeBindings() {
        try {
            return (ScriptObjectMirror) ((Invocable) scriptEngine).invokeFunction("definitionNewObject");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // 定位文件位置
    private String resolve(String base, String name) {
        // 删除多余的路径分隔符“/”
        if (base.endsWith("/") && name.startsWith("/")) {
            name = name.substring(0, name.length() - 1);
        }
        if (base.endsWith(File.separator) && name.startsWith(File.separator)) {
            name = name.substring(0, name.length() - 1);
        }
        // 使用的是远程资源
        if (base.startsWith("http://") || base.startsWith("https://")) {
            // 自动补全“.js”后缀
            if (!name.endsWith(".js")) {
                name += ".js";
            }
            return base + name;
        }
        // 使用本地资源
        String[] tryPath = new String[]{
                FilenameUtils.concat(base, name),
                FilenameUtils.concat(base, name + ".js"),
        };
        for (String path : tryPath) {
            File file = new File(path);
            if (file.exists() && file.isFile()) {
                return path.replace(File.separatorChar, '/');
            }
        }
        throw new RuntimeException(String.format("base=%s, name=%s | 文件不存在", base, name));
    }

    private static class DefaultRequire implements RequireFunction {
        // 当前线程缓存(fileFullPath --> ScriptObjectMirror缓存)
        private final static ThreadLocal<Map<String, ScriptObjectMirror>> refCache = new ThreadLocal<>();

        private final YvanJS yvanJS;

        DefaultRequire(YvanJS yvanJS) {
            this.yvanJS = yvanJS;
        }

        @Override
        public ScriptObjectMirror require(String name) {
            if (name == null) {
                throwModuleNotFoundException("<null>");
            }
            // 让我们确保每个线程都有自己的refCache
            boolean needRemove = false;
            if (refCache.get() == null) {
                needRemove = true;
                refCache.set(new HashMap<>());
            }
            // 获取 Module ID
            String id = yvanJS.resolve(yvanJS.properties.getBase(), name);
            Module found = yvanJS.cache.get(id);
            if (found != null) {
                log.debug("# ModuleCache 命中缓存 -> {}", id);
                return found.getExports();
            }
            ScriptObjectMirror cachedExports = refCache.get().get(id);
            if (cachedExports != null) {
                log.debug("# RefCache 命中缓存 -> {}", id);
                return cachedExports;
            } else {
                // 我们必须存储对当前加载模块的引用，以避免循环require
                log.debug("# RefCache 加入缓存 -> {}", id);
                refCache.get().put(id, yvanJS.createSafeBindings());
            }
            // 加载 Module
            try {
                // 寻找并加载 Module
                Bindings bindings = new SimpleBindings();
                bindings.put("id", id);
                bindings.put("console", new Console(id));
                found = (Module) yvanJS.scriptEngine.eval("load('" + id + "');", bindings);
                // 设置加载成功
                found.setLoaded();
                // 缓存当前加载的 Module
                log.debug("# ModuleCache 加入缓存(require) -> {}", id);
                yvanJS.cache.put(id, found);
                // children.add(found.module);
                return found.getExports();
            } catch (ScriptException e) {
                throw new RuntimeException(e);
            } finally {
                //  需要删除防止内存泄漏
                if (needRemove && refCache.get() != null) {
                    refCache.remove();
                }
            }
        }

        // 抛出Module找不到异常
        private void throwModuleNotFoundException(String module) {
            throw new ECMAException("Module not found: " + module, null);
        }
    }

    private static class DefaultDefine implements DefineFunction {
        private final YvanJS yvanJS;

        DefaultDefine(YvanJS yvanJS) {
            this.yvanJS = yvanJS;
        }

        @Override
        public Module define(ScriptObjectMirror factory) {
            String id;
            Module module;
            if (factory.isFunction()) {
                //factory 是函数的情况
                Source source = $.source(factory);
                id = source.getName();
                module = new Module(id, yvanJS.createSafeBindings(), yvanJS.createSafeBindings());
                // this         --> module
                // require      --> yvanJS.require (实现了RequireFunction接口)
                // exports      --> module.exports
                // module       --> module.module
                Object result = factory.call(module, yvanJS.require, module.getExports(), module.getModule());
                if (result instanceof ScriptObjectMirror) {
                    module.setExports((ScriptObjectMirror) result);
                }
            } else {
                //factory 是对象本身
                id = (String) factory.eval("id");
                module = new Module(id, yvanJS.createSafeBindings(), yvanJS.createSafeBindings());
                module.setExports(factory);
            }
            return module;
        }
    }

    /**
     * 基于内存的缓存
     */
    private static class MemoryModuleCache implements ModuleCache {
        private Map<String, Module> modules = new HashMap<>();

        @Override
        public Module get(String fullPath) {
            return modules.get(fullPath);
        }

        @Override
        public void put(String fullPath, Module module) {
            modules.put(fullPath, module);
        }

        @Override
        public void clear() {
            modules.clear();
        }

        @Override
        public int cacheSize() {
            return modules.size();
        }
    }
}
